package com.george.auth.service;

import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.george.auth.cache.RedisCacheServiceImpl;
import com.george.auth.constants.TokenConstants;
import com.george.auth.vo.LoginUserVO;
import com.george.common.entity.dto.user.LoginUserDTO;
import com.george.common.exception.BaseException;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Base64Util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

/**
 * @title: TokenServiceImpl.java
 * @description: 用户登录token Service实现
 * @author: George
 * @date: 2023/6/13 15:19
 */
@Component
@Slf4j
public class TokenServiceImpl {

    /**
     * 令牌有效期（默认30分钟）
     */
    @Value("${sys.token.expireTime:30}")
    private int expireTime;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TWE = 20 * 60 * 1000L;

    @Resource
    private RedisCacheServiceImpl redisCacheService;

    /**
     * 构建jwt
     *
     * @param loginUser
     * @return
     */
    public static String createJwt(LoginUserVO loginUser) {
        try {
            //将密码置空
            loginUser.getUser().setPassword("");
            //userId是重要信息，进行加密下
            String encodeUserId = Base64Util.encode(String.valueOf(loginUser.getUser().getId()));
            String encodeUserInfo = Base64Util.encode(JSON.toJSONString(loginUser.getUser()));
            //添加构成JWT的参数
            JSONObject json = JSONObject.parseObject(JSON.toJSONString(loginUser.getUser()));
            if (Objects.nonNull(json)) {
                //前端解析token里面的Long类型id会有精度丢失问题，这里将long类型的id替换为String类型
                json.put("id", String.valueOf(loginUser.getUser().getId()));
            }
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以将基本不重要的对象信息放到claims
                    .claim("userInfo", encodeUserInfo)
                    .claim("userId", encodeUserId)
                    .claim(TokenConstants.LOGIN_TOKEN_CHANNEL_KEY, loginUser.getTokenChannel())
                    .claim(TokenConstants.LOGIN_USER_KEY, loginUser.getToken())
                    // 代表这个JWT的主体，即它的所有人
                    .setSubject(json.toJSONString())
                    // 是一个时间戳，代表这个JWT的签发时间；
                    .setIssuedAt(new Date(loginUser.getLoginTime()))
                    // 签名密钥；
                    .signWith(SignatureAlgorithm.HS256, TokenConstants.JWT_SECRET);
            //生成JWT
            return TokenConstants.TOKEN_PREFIX + builder.compact();
        } catch (Exception e) {
            log.error("签名失败", e);
            throw new BaseException("登录签名生成失败");
        }
    }
    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUserVO getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.hasText(token)) {
            try {
                LoginUserVO user = new LoginUserVO();
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(TokenConstants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                //从redis中返回
                user = redisCacheService.get(userKey, LoginUserVO.class);
                return user;
            } catch (Exception e) {
                log.error("获取用户身份信息异常：{}", e);
            }
        }
        return null;
    }


    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUserVO loginUser) {
        if (StringUtils.hasText(loginUser.getToken())) {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String token) {
        if (StringUtils.hasText(token)) {
            String userKey = getTokenKey(token);
            redisCacheService.delete(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public LoginUserDTO createToken(LoginUserVO loginUser) {

        String tokenId = UUID.randomUUID().toString();
        loginUser.setToken(tokenId);
        refreshToken(loginUser);
        return LoginUserDTO.builder()
                .token(createJwt(loginUser))
                .id(loginUser.getUser().getId())
                .username(loginUser.getUser().getUsername())
                .name(loginUser.getUser().getName())
                .alisa(loginUser.getUser().getAlisa())
                .age(loginUser.getUser().getAge())
                .height(loginUser.getUser().getHeight())
                .weight(loginUser.getUser().getWeight())
                .email(loginUser.getUser().getEmail())
                .sex(loginUser.getUser().getSex())
                .expireTime(loginUser.getExpireTime())
                .loginTime(loginUser.getLoginTime())
                .build();
    }

    /**
     * 验证令牌有效期，相差不足20分钟，自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    public void verifyToken(LoginUserVO loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TWE) {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUserVO loginUser) {
        // 设置token的有效期
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCacheService.put(userKey, loginUser, expireTime * 60);
    }


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, TokenConstants.JWT_SECRET).compact();
        return token;
    }


    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(TokenConstants.JWT_SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            /**
             * 不管是否过期，都返回claims对象，由于jwt创建token是根据jwt保存的相关信息来计算的，过期时间是其中的一个计算维度，
             * 所以一旦过期时间改了，那么生成的token值也就变了,故不能刷新claims的过期时间
             * token过期放redis校验，刷新redis的过期时间
             */
            log.info("jwt token对象转换Claims 异常，token数据：{},异常原因：jwt过期", token);
            claims = e.getClaims();
        }
        return claims;
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(TokenConstants.HEADER);
        if (StringUtils.hasText(token)) {
            if (token.startsWith(TokenConstants.TOKEN_PREFIX)) {
                token = token.replace(TokenConstants.TOKEN_PREFIX, "");
            } else {
                return null;
            }
        }
        return token;
    }


    private String getTokenKey(String uuid) {
        return TokenConstants.LOGIN_TOKEN_KEY + uuid;
    }

}
